WebGL 交错顶点布局与索引缓冲复用
一句话结论:交错布局让同一顶点的属性连续存放以提高缓存命中,索引缓冲让顶点被多个三角形共用,两者结合是顶点数据优化的第一步。
交错与分离顶点布局
是什么
顶点布局描述顶点属性在内存中的排列方式。交错布局把同一顶点的位置、法线、UV 等属性连续存放,多个顶点依次排开;分离布局把同一种属性全部放在一段缓冲里,不同属性各占一段。
解决什么问题
它解决的是顶点读取效率问题。GPU 读取顶点属性时按缓存行加载数据,如果同一顶点的属性分散在不同缓冲中,一次读取就要跳转多个内存区域,缓存命中率下降;交错存放则让一次加载就把整个顶点读进缓存。
为什么这样做
顶点着色器对每个顶点的处理是独立的,但执行单元同时处理相邻顶点。把相邻顶点的数据放在连续地址上,硬件预取就能一次命中多个顶点,因此交错布局更契合 GPU 的访问模式。分离布局的优势在于更新频率差异大的属性可以单独更新,不必重写整段顶点数据。
怎么用
// 交错布局:位置 3 个 float + UV 2 个 float,共 20 字节
const STRIDE = 5 * 4;
const interleaved = new Float32Array([
// x, y, z, u, v
-0.5, -0.5, 0, 0, 0,
0.5, -0.5, 0, 1, 0,
0.0, 0.5, 0, 0.5, 1,
]);
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, interleaved, gl.STATIC_DRAW);
gl.vertexAttribPointer(aPosition, 3, gl.FLOAT, false, STRIDE, 0);
gl.vertexAttribPointer(aTexCoord, 2, gl.FLOAT, false, STRIDE, 3 * 4);
| 对比项 | 交错布局 | 分离布局 |
|---|---|---|
| 内存局部性 | 好,一个顶点连续 | 差,属性间跨度大 |
| 缓存命中率 | 高 | 较低 |
| 局部更新 | 需重写相邻顶点区间 | 可单独更新某类属性 |
| 缓冲数量 | 1 个 | 每个属性 1 个 |
常见坑
- 交错布局却按单属性设置步长 → 原因:漏改
stride→ 处理:步长统一为单顶点字节数 - 全部用分离布局导致属性绑定过多 → 原因:忽略顶点属性数量上限 → 处理:优先合并为交错布局
- 局部更新交错数据时整段重传 → 原因:未按顶点区间计算偏移 → 处理:用
bufferSubData精确写入目标区间
索引缓冲与顶点复用
是什么
索引缓冲(IBO)是存放顶点序号的一段缓冲,配合 drawElements 使用。绘制时 GPU 按索引顺序取顶点,同一个顶点可以被多个三角形重复引用,从而避免重复存储。
解决什么问题
它解决顶点数据冗余问题。用 drawArrays 绘制三角形列表时,每个三角形都要独占三个顶点,共享顶点被反复存储;一个规则网格的顶点会被六个三角形共用,冗余率非常高,索引缓冲把顶点数量降到接近网格点数。
为什么这样做
顶点数据的主要成本在传输与显存占用,而不在索引本身。索引使用 16 位无符号整数时,每个三角形只需 6 字节索引,远小于一个顶点的存储开销。因此设计上把“顶点数据”和“组织方式”分离,让同一份顶点被多次引用。
怎么用
// 4 个顶点组成一个矩形,用 6 个索引画出两个三角形
const positions = new Float32Array([
-0.5, -0.5, 0,
0.5, -0.5, 0,
0.5, 0.5, 0,
-0.5, 0.5, 0,
]);
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
gl.vertexAttribPointer(aPosition, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(aPosition);
// 逆时针缠绕,保证面朝向正确
const indices = new Uint16Array([0, 1, 2, 0, 2, 3]);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);
常见坑
- 索引值超出顶点数量 → 原因:索引从 0 开始编号,写成了 1 起 → 处理:核对最大索引等于顶点数减一
- 顶点数超过 65535 仍用
UNSIGNED_SHORT→ 原因:16 位溢出 → 处理:改用UNSIGNED_INT(需 WebGL 2.0 或对应扩展) - 面显示不全 → 原因:缠绕顺序与剔除设置冲突 → 处理:统一逆时针并检查
cullFace与frontFace - 索引缓冲绑定被后续操作覆盖 → 原因:
ELEMENT_ARRAY_BUFFER绑定属于顶点数组状态 → 处理:绘制前重新绑定确保状态正确
要点总结
- 交错布局把同一顶点的属性连续存放,缓存命中率更高
- 分离布局适合属性更新频率差异大的场景,但缓冲数量更多
- 索引缓冲用序号复用顶点,网格类数据可显著降低顶点存储量
- 索引从 0 开始,最大索引等于顶点数减一
- 缠绕顺序决定面朝向,与面剔除设置必须一致
